Skip to content

feat: Riksrevisionen follow-up tracker with skrivelse deadlines and accountability monitoring#2040

Merged
pethers merged 8 commits intomainfrom
copilot/add-riksrevisionen-tracker
Apr 27, 2026
Merged

feat: Riksrevisionen follow-up tracker with skrivelse deadlines and accountability monitoring#2040
pethers merged 8 commits intomainfrom
copilot/add-riksrevisionen-tracker

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

RiR findings (HD01JuU31, HC03206) were cited passively in intelligence assessments with no tracking of constitutional response deadlines or government accountability status. Swedish constitutional practice requires a government skrivelse within 4 months of each RiR publication — this was invisible in the platform.

New dataset & schema

  • data/rir-followups.json — seed records for 4 RiR reports with publish_date, skrivelse_deadline, gov_response_status (PENDING/RESPONDED/OVERDUE/PARTIAL), response_skrivelse_id, committees, and risk_level
  • schemas/rir-followups-schema.json — JSON Schema draft-07 for CI validation; uses an if/then conditional rule to enforce that gov_response_status: "RESPONDED" requires a non-empty response_skrivelse_id (ajv-verified)

Library: scripts/rir-followups-client.ts

Pure-TypeScript module with injectable I/O (static node:fs imports, ESM-safe; no I/O at import time):

calculateSkrivelseDeadline('2026-01-15')         // '2026-05-15' (adds N calendar months,
                                                  // preserves day-of-month, clamps to month-end if shorter)
// `calculateSkrivelsDeadline` is exported as a deprecated alias for backwards compatibility.

deriveResponseStatus(record, asOf)               // derives primarily from response_skrivelse_id presence:
                                                  //   skrivelse_id + open_recommendations>0 → PARTIAL
                                                  //   skrivelse_id + open_recommendations==0 → RESPONDED
                                                  //   else: PENDING/OVERDUE vs. deadline
daysOverdue(deadlineDate, asOf)                  // throws RangeError on invalid input
                                                  // (mirrors calculateSkrivelseDeadline)
detectOverdueAlerts(dataset, asOf)               // sorted by days_overdue desc; uses DEFAULT_RISK_LEVEL
injectRirTableIntoDocument(md, records, asOf)    // idempotent inject/replace between HTML comment markers
filterByCommittee / filterByStatus / filterByMinRiskLevel
                                                  // filterByMinRiskLevel uses the same DEFAULT_RISK_LEVEL
                                                  // ('MEDIUM') for missing risk_level
validateRirRecord / validateRirDataset           // type-checks response_skrivelse_id (string|null) and
                                                  // committees[] / parliamentary_followup_doc_ids[] item types
loadRirDataset(path, readFn) / saveRirDataset(dataset, path, writeFn, nowDate?)
                                                  // saveRirDataset accepts an injectable clock for
                                                  // deterministic `last_updated` in tests

A shared DEFAULT_RISK_LEVEL = 'MEDIUM' constant is now used by both detectOverdueAlerts and filterByMinRiskLevel so missing risk_level is treated consistently across alerting and filtering.

CLI: scripts/fetch-rir-followups.ts

Daily update script: queries data.riksdagen.se for doktyp=skr over a 90-day window (UTC-safe setUTCDate/getUTCDate arithmetic), with paginated results (p=N until exhausted, with a hard 50-page safety cap) so 90-day windows containing more than one page of skrivelser no longer silently truncate. Matches against known records via rir_report_id/rir_number in the title and via tolerant equality between skrivelse.beteckning and the stored response_skrivelse_id (normalisation strips the "Skr." prefix, whitespace, and case). When a match is found the persisted status is computed via deriveResponseStatus on a candidate record so a response with open_recommendations > 0 correctly stays PARTIAL rather than being forced to RESPONDED. Persists changes only when records actually change (overdue alerts alone do not trigger noisy rewrites). --date is validated and exits 1 with a clear error on invalid input; --date without a following value exits 2 with a clear "missing value" message. --alert flag exits 1 when any OVERDUE records are detected.

Constitutional citations

References are aligned across data/rir-followups.json and scripts/rir-followups-client.ts: RO 10:4 for Riksdag handling of Riksrevisionen reports, together with RF 5:4 and RF ch. 13:7 for government accountability/follow-up context, treated as complementary rather than conflicting bases.

Skill update

legislative-monitoring/SKILL.md gains a §7 Riksrevisionen Follow-Up Tracking section: constitutional basis, data model table, stateDiagram-v2 lifecycle, library API reference (canonical calculateSkrivelseDeadline), CLI usage, Markdown injection pattern, MCP search query (doktyp=skr), and alert-threshold table (0/1–30/31–90/91+ days).

Tests

68 Vitest tests in tests/rir-followups-client.test.ts covering all exported functions — including the deprecated alias, deriveResponseStatus rule-1 derivation (skrivelse-id-implies-RESPONDED), PARTIAL detection via open_recommendations, type-check validation for response_skrivelse_id / committees[] / parliamentary_followup_doc_ids[], daysOverdue RangeError on invalid input, the shared DEFAULT_RISK_LEVEL semantics for filterByMinRiskLevel, and a deterministic-clock test for saveRirDataset — plus an integration test that loads and validates the real data/rir-followups.json on every test run.

@github-actions github-actions Bot added the size-xs Extra small change (< 10 lines) label Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🏷️ Automatic Labeling Summary

This PR has been automatically labeled based on the files changed and PR metadata.

Applied Labels: size-xs

Label Categories

  • 🗳️ Content: news, dashboard, visualization, intelligence
  • 💻 Technology: html-css, javascript, workflow, security
  • 📊 Data: cia-data, riksdag-data, data-pipeline, schema
  • 🌍 I18n: i18n, translation, rtl
  • 🔒 ISMS: isms, iso-27001, nist-csf, cis-controls
  • 🏗️ Infrastructure: ci-cd, deployment, performance, monitoring
  • 🔄 Quality: testing, accessibility, documentation, refactor
  • 🤖 AI: agent, skill, agentic-workflow

For more information, see .github/labeler.yml.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

…a, schema, tests, skill)

Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/9537ad08-73e5-4552-8cbb-3702166ebd61

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
@github-actions github-actions Bot added documentation Documentation updates javascript JavaScript code changes schema Data schema changes testing Test coverage refactor Code refactoring skill Skill configuration size-xl Extra large change (> 1000 lines) labels Apr 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Comment thread scripts/fetch-rir-followups.ts Fixed
…arity, PARTIAL→RESPONDED transition, deduplicated imports)

Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/9537ad08-73e5-4552-8cbb-3702166ebd61

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Riksrevisionen follow-up tracker with response deadlines feat: Riksrevisionen follow-up tracker with skrivelse deadlines and accountability monitoring Apr 27, 2026
Copilot AI requested a review from pethers April 27, 2026 01:04
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers marked this pull request as ready for review April 27, 2026 08:05
Copilot AI review requested due to automatic review settings April 27, 2026 08:05
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers requested a review from Copilot April 27, 2026 10:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 14 comments.

Comment on lines +185 to +207
export function deriveResponseStatus(
record: RirFollowUpRecord,
asOf: Date | string = new Date(),
): RirResponseStatus {
const now = typeof asOf === 'string' ? new Date(asOf + 'T00:00:00Z') : asOf;

// Already marked responded or partial explicitly — respect it unless no skrivelse ID
if (record.gov_response_status === 'RESPONDED' && record.response_skrivelse_id) {
return 'RESPONDED';
}
if (record.gov_response_status === 'PARTIAL' && record.response_skrivelse_id) {
return 'PARTIAL';
}

// No response yet — check deadline
if (record.skrivelse_deadline) {
const deadline = new Date(record.skrivelse_deadline + 'T00:00:00Z');
if (now > deadline && !record.response_skrivelse_id) {
return 'OVERDUE';
}
}

return 'PENDING';
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deriveResponseStatus() does not implement the documented rule that a set response_skrivelse_id implies RESPONDED/PARTIAL. Right now, a record with gov_response_status: 'PENDING' but a non-null response_skrivelse_id will still return PENDING (or OVERDUE), which makes status derivation inconsistent with the data model and CLI updates. Consider deriving primarily from response_skrivelse_id (and optionally open_recommendations/explicit flag) rather than requiring the stored status to already be RESPONDED/PARTIAL.

Copilot uses AI. Check for mistakes.
Comment thread scripts/rir-followups-client.ts Outdated
Comment on lines +145 to +167
export function calculateSkrivelsDeadline(
publishDate: string,
options: DeadlineCalculatorOptions = {},
): string {
const months = options.monthsOverride ?? CONSTITUTIONAL_DEADLINE_MONTHS;
const pub = new Date(publishDate + 'T00:00:00Z');
if (isNaN(pub.getTime())) {
throw new RangeError(`Invalid publish_date: ${publishDate}`);
}
const year = pub.getUTCFullYear();
const month = pub.getUTCMonth(); // 0-indexed
const day = pub.getUTCDate();

const deadlineYear = year + Math.floor((month + months) / 12);
const deadlineMonth = (month + months) % 12; // 0-indexed

// Clamp to last day of deadline month
const daysInDeadlineMonth = new Date(Date.UTC(deadlineYear, deadlineMonth + 1, 0)).getUTCDate();
const deadlineDay = Math.min(day, daysInDeadlineMonth);

const pad = (n: number): string => String(n).padStart(2, '0');
return `${deadlineYear}-${pad(deadlineMonth + 1)}-${pad(deadlineDay)}`;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name calculateSkrivelsDeadline appears to have a typo (“Skrivels” vs “Skrivelse”). Since this is a new public API, consider renaming to calculateSkrivelseDeadline (and optionally keeping the old name as a deprecated alias) to avoid cementing the misspelling across scripts/docs/tests.

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
* @license Apache-2.0
*/

import fs from 'node:fs';
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs is imported but never used, which will fail linting in this repo’s ESLint setup. Remove the unused import (or use node:fs via the injected read/write fns if you intended to rely on it).

Suggested change
import fs from 'node:fs';

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
}
}

const asOf = dateStr ? new Date(dateStr + 'T00:00:00Z') : new Date();
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseArgs() accepts --date but does not validate it. If an invalid value is passed, asOf becomes Invalid Date and later opts.asOf.toISOString() will throw, producing a less actionable failure. Validate asOf (e.g., isNaN(asOf.getTime())) and exit with a clear error message when --date is invalid.

Suggested change
const asOf = dateStr ? new Date(dateStr + 'T00:00:00Z') : new Date();
const asOf = dateStr ? new Date(`${dateStr}T00:00:00Z`) : new Date();
if (Number.isNaN(asOf.getTime())) {
console.error('[fetch-rir-followups] Invalid --date value. Expected YYYY-MM-DD.');
process.exit(1);
}

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
Comment on lines +272 to +278
// Persist
if (!opts.dryRun) {
if (updatedCount > 0 || alerts.length > 0) {
saveRirDataset(updatedDataset, opts.dataFile);
console.log(`[fetch-rir-followups] Dataset saved (${updatedCount} record(s) updated).`);
} else {
console.log('[fetch-rir-followups] No changes — dataset not rewritten.');
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveRirDataset() is invoked when alerts.length > 0 even if updatedCount === 0, but alert detection does not mutate the dataset. This will rewrite the JSON (and bump last_updated) on every run whenever any overdue item exists, creating noisy diffs/commits. Consider only saving when records actually change, or explicitly mutate stored statuses before saving if the intent is to persist derived OVERDUE state.

Suggested change
// Persist
if (!opts.dryRun) {
if (updatedCount > 0 || alerts.length > 0) {
saveRirDataset(updatedDataset, opts.dataFile);
console.log(`[fetch-rir-followups] Dataset saved (${updatedCount} record(s) updated).`);
} else {
console.log('[fetch-rir-followups] No changes — dataset not rewritten.');
// Persist only when stored records actually change.
// Overdue alerts are derived from the dataset and do not mutate persisted state.
if (!opts.dryRun) {
if (updatedCount > 0) {
saveRirDataset(updatedDataset, opts.dataFile);
console.log(`[fetch-rir-followups] Dataset saved (${updatedCount} record(s) updated).`);
} else {
console.log('[fetch-rir-followups] No record changes — dataset not rewritten.');

Copilot uses AI. Check for mistakes.
Comment thread tests/rir-followups-client.test.ts Outdated
Comment on lines +559 to +566
it('updates last_updated to today', () => {
let written = '';
const mockWrite = (_path: string, data: string) => { written = data; };
saveRirDataset(DATASET, '/fake/path.json', mockWrite);
const parsed = JSON.parse(written) as RirFollowUpsDataset;
const today = new Date().toISOString().slice(0, 10);
expect(parsed.last_updated).toBe(today);
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test updates last_updated to today can be flaky around UTC midnight because it compares two separate new Date() calls (one inside saveRirDataset, one in the test). Use Vitest fake timers (vi.useFakeTimers() + vi.setSystemTime(...)) or inject a clock into saveRirDataset to make this deterministic.

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
Comment on lines +225 to +227
// PARTIAL → RESPONDED when a new (or fuller) skrivelse is found
const newStatus: RirFollowUpRecord['gov_response_status'] =
prevStatus === 'PARTIAL' && record.response_skrivelse_id ? 'RESPONDED' : 'RESPONDED';
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newStatus is always set to 'RESPONDED' (the conditional expression returns 'RESPONDED' on both branches), which makes the comment about “PARTIAL → RESPONDED” misleading and leaves dead logic. Either simplify this assignment, or implement the intended branching (e.g., only upgrade PARTIAL when a newer/full response is detected).

Suggested change
// PARTIAL → RESPONDED when a new (or fuller) skrivelse is found
const newStatus: RirFollowUpRecord['gov_response_status'] =
prevStatus === 'PARTIAL' && record.response_skrivelse_id ? 'RESPONDED' : 'RESPONDED';
// Mark the record as responded when a matching skrivelse is found
const newStatus: RirFollowUpRecord['gov_response_status'] = 'RESPONDED';

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +125
"gov_response_status": {
"type": "string",
"enum": ["PENDING", "RESPONDED", "OVERDUE", "PARTIAL"],
"description": "Government response status: PENDING=awaiting response, RESPONDED=fully responded, OVERDUE=deadline elapsed without response, PARTIAL=partial response only"
},
"response_skrivelse_id": {
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The Riksdag document ID or reference of the government skrivelse response (null if not yet responded)"
},
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the JSON Schema enforces that gov_response_status: "RESPONDED" requires a non-null response_skrivelse_id, but the schema currently has no conditional if/then (or similar) to enforce that relationship. Add a conditional schema rule (draft-07 supports if/then) to ensure CI validation matches the stated contract.

Copilot uses AI. Check for mistakes.
Comment thread data/rir-followups.json Outdated
"last_updated": "2026-04-27",
"constitutional_deadline_months": 4,
"notes": [
"Swedish constitutional practice (RF 5:4 + RO 10:4) requires the government to formally respond to each Riksrevisionen audit report with a skrivelse (written communication) to the Riksdag.",
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Constitutional citations are inconsistent across the newly added artifacts: this dataset note cites “RF 5:4 + RO 10:4”, while scripts/rir-followups-client.ts cites “RF ch. 13:7 + RO 10:4”. Align the references (or clarify why multiple provisions apply) so users don’t propagate conflicting constitutional bases into assessments.

Suggested change
"Swedish constitutional practice (RF 5:4 + RO 10:4) requires the government to formally respond to each Riksrevisionen audit report with a skrivelse (written communication) to the Riksdag.",
"Swedish constitutional and parliamentary practice cites RO 10:4 for Riksdag handling of Riksrevisionen reports, together with RF 5:4 and, in some assessments, RF ch. 13:7 for the government's accountability and follow-up context; this dataset treats those provisions as complementary rather than conflicting bases for the formal skrivelse response expectation.",

Copilot uses AI. Check for mistakes.
Comment thread scripts/rir-followups-client.ts Outdated
Comment on lines +132 to +134
* The returned date is the last day of the 4th full calendar month following
* the publish month (i.e. add 4 months, keep same day-of-month unless it
* falls beyond the end of that month, in which case use last day).
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc for calculateSkrivelsDeadline says “returned date is the last day of the 4th full calendar month”, but the implementation keeps the same day-of-month and only clamps to month-end when needed. Reword the description to match the actual rule (add N calendar months; clamp to month-end if target month is shorter).

Suggested change
* The returned date is the last day of the 4th full calendar month following
* the publish month (i.e. add 4 months, keep same day-of-month unless it
* falls beyond the end of that month, in which case use last day).
* The returned date is computed by adding the configured number of calendar
* months to the publish date and preserving the original day-of-month when
* possible; if the target month is shorter, the date is clamped to that
* month's last day.

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Copy Markdown
Member

pethers commented Apr 27, 2026

@copilot apply changes based on the comments in this thread and fix issues in failing checks
TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request)
TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request)Failing after 46s

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

…e, ESM-safe fs imports, fix dead branching, schema if/then, deterministic clock, type validation, UTC arithmetic

Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/3a83a573-e176-4b18-a981-d439c3e6596b

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot apply changes based on the comments in this thread and fix issues in failing checks
[TypeS...

Addressed in fd10161. CI failure (unused fs import) fixed plus all 13 review-thread items: rename calculateSkrivelsDeadlinecalculateSkrivelseDeadline (deprecated alias kept), ESM-safe static fs imports, deriveResponseStatus now derives from response_skrivelse_id presence (rule 1), validation type-checks for response_skrivelse_id/arrays, UTC-safe date window arithmetic, --date validation, removed dead PARTIAL→RESPONDED branch, schema if/then for RESPONDED↔skrivelse (ajv-verified), save only on actual record changes, injectable clock for deterministic last_updated test, JSDoc/citation/test-header alignment. 66 RiR tests + full suite (2406 tests) pass; tsc and eslint clean.

Signed-off-by: James Pether Sörling <pethers@users.noreply.github.com>
@pethers pethers requested a review from Copilot April 27, 2026 12:29
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Comment thread scripts/fetch-rir-followups.ts Outdated
Comment on lines +68 to +69
else if (arg === '--date' && args[i + 1]) {
dateStr = args[++i];
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseArgs silently ignores --date when it is the last argument (no value provided). That can lead to running with today's date unexpectedly. Consider treating --date without a following value as a usage error (print message + exit 1/2).

Suggested change
else if (arg === '--date' && args[i + 1]) {
dateStr = args[++i];
else if (arg === '--date') {
const nextArg = args[i + 1];
if (!nextArg) {
console.error('[fetch-rir-followups] Missing value for --date. Expected YYYY-MM-DD.');
process.exit(2);
}
dateStr = nextArg;
i++;

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
Comment on lines +96 to +132
const params = new URLSearchParams({
doktyp: 'skr',
from: fromDate,
tom: toDate,
sz: String(limit),
utformat: 'json',
sort: 'datum',
sortorder: 'desc',
});

const url = `https://data.riksdagen.se/dokumentlista/?${params.toString()}`;

let response: Response;
try {
response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
headers: { Accept: 'application/json' },
});
} catch (err) {
console.error(`[fetch-rir-followups] Network error fetching Riksdag API: ${err}`);
return [];
}

if (!response.ok) {
console.error(`[fetch-rir-followups] Riksdag API returned ${response.status}`);
return [];
}

let json: RiksdagDocumentListResponse;
try {
json = (await response.json()) as RiksdagDocumentListResponse;
} catch {
console.error('[fetch-rir-followups] Failed to parse Riksdag API JSON response');
return [];
}

return json?.dokumentlista?.dokument ?? [];
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchRiksdagSkrivelser hard-limits results to 50 (sz) and does not paginate. Over a 90-day window it’s plausible to exceed this and miss matching skrivelser, leaving records stuck as PENDING/OVERDUE. Consider paging through results until exhausted (or increasing sz and still paging as a safety net).

Suggested change
const params = new URLSearchParams({
doktyp: 'skr',
from: fromDate,
tom: toDate,
sz: String(limit),
utformat: 'json',
sort: 'datum',
sortorder: 'desc',
});
const url = `https://data.riksdagen.se/dokumentlista/?${params.toString()}`;
let response: Response;
try {
response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
headers: { Accept: 'application/json' },
});
} catch (err) {
console.error(`[fetch-rir-followups] Network error fetching Riksdag API: ${err}`);
return [];
}
if (!response.ok) {
console.error(`[fetch-rir-followups] Riksdag API returned ${response.status}`);
return [];
}
let json: RiksdagDocumentListResponse;
try {
json = (await response.json()) as RiksdagDocumentListResponse;
} catch {
console.error('[fetch-rir-followups] Failed to parse Riksdag API JSON response');
return [];
}
return json?.dokumentlista?.dokument ?? [];
const documents: RiksdagDocumentResult[] = [];
let page = 1;
while (true) {
const params = new URLSearchParams({
doktyp: 'skr',
from: fromDate,
tom: toDate,
sz: String(limit),
p: String(page),
utformat: 'json',
sort: 'datum',
sortorder: 'desc',
});
const url = `https://data.riksdagen.se/dokumentlista/?${params.toString()}`;
let response: Response;
try {
response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
headers: { Accept: 'application/json' },
});
} catch (err) {
console.error(`[fetch-rir-followups] Network error fetching Riksdag API: ${err}`);
return [];
}
if (!response.ok) {
console.error(`[fetch-rir-followups] Riksdag API returned ${response.status}`);
return [];
}
let json: RiksdagDocumentListResponse;
try {
json = (await response.json()) as RiksdagDocumentListResponse;
} catch {
console.error('[fetch-rir-followups] Failed to parse Riksdag API JSON response');
return [];
}
const pageDocuments = json?.dokumentlista?.dokument ?? [];
documents.push(...pageDocuments);
if (pageDocuments.length === 0 || pageDocuments.length < limit) {
break;
}
page += 1;
}
return documents;

Copilot uses AI. Check for mistakes.
Comment on lines +167 to +178
const title = (skrivelse.titel ?? '').toLowerCase();
const docId = skrivelse.dok_id ?? skrivelse.id ?? '';

for (const record of records) {
// Match on rir_report_id in title
if (title.includes(record.rir_report_id.toLowerCase())) return record;
// Match on rir_number (e.g. "RiR 2026:6") in title
const rirNum = record.rir_number.toLowerCase();
if (title.includes(rirNum)) return record;
// Match on response_skrivelse_id (document already matched)
if (record.response_skrivelse_id && docId.includes(record.response_skrivelse_id)) return record;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matchSkrivelse tries to match an already-recorded response_skrivelse_id by checking whether docId (dok_id/id) includes the stored value. In the dataset the field is a human reference like Skr. 2025/26:78, which won’t be contained in a dok_id like H.... Match against skrivelse.beteckning (or normalize both sides) instead, otherwise previously-responded records may never match.

Copilot uses AI. Check for mistakes.
Comment thread scripts/fetch-rir-followups.ts Outdated
Comment on lines +230 to +240
// Mark the record as responded when a matching skrivelse is found.
// PARTIAL records are upgraded to RESPONDED when a (presumably fuller) skrivelse appears.
const newStatus: RirFollowUpRecord['gov_response_status'] = 'RESPONDED';
console.log(
`[fetch-rir-followups] Matched response for ${record.rir_report_id} (${prevStatus} → ${newStatus}): ${newId}`,
);
updatedCount++;
return {
...record,
gov_response_status: newStatus,
response_skrivelse_id: newId,
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a matching skrivelse is found, the script always writes gov_response_status: 'RESPONDED' even if open_recommendations > 0. That conflicts with the library’s derivation rules (response + open recommendations => PARTIAL) and can persist an incorrect status to data/rir-followups.json. Consider deriving the stored status from open_recommendations (and/or preserving PARTIAL unless you have evidence the new skrivelse closes recommendations).

Suggested change
// Mark the record as responded when a matching skrivelse is found.
// PARTIAL records are upgraded to RESPONDED when a (presumably fuller) skrivelse appears.
const newStatus: RirFollowUpRecord['gov_response_status'] = 'RESPONDED';
console.log(
`[fetch-rir-followups] Matched response for ${record.rir_report_id} (${prevStatus}${newStatus}): ${newId}`,
);
updatedCount++;
return {
...record,
gov_response_status: newStatus,
response_skrivelse_id: newId,
const candidateRecord: RirFollowUpRecord = {
...record,
response_skrivelse_id: newId,
};
// Derive persisted status from the updated record so stored data stays
// consistent with the canonical library rules (for example, a response
// with open recommendations remains PARTIAL rather than being forced to
// RESPONDED).
const newStatus = deriveResponseStatus(candidateRecord, opts.asOf);
console.log(
`[fetch-rir-followups] Matched response for ${record.rir_report_id} (${prevStatus}${newStatus}): ${newId}`,
);
updatedCount++;
return {
...candidateRecord,
gov_response_status: newStatus,

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +244
export function daysOverdue(
deadlineDate: string,
asOf: Date | string = new Date(),
): number {
const now = typeof asOf === 'string' ? new Date(asOf + 'T00:00:00Z') : asOf;
const deadline = new Date(deadlineDate + 'T00:00:00Z');
const diffMs = now.getTime() - deadline.getTime();
return diffMs > 0 ? Math.floor(diffMs / (1000 * 60 * 60 * 24)) : 0;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

daysOverdue returns 0 for invalid deadlineDate / asOf values because Invalid Date produces NaN and diffMs > 0 becomes false. Since calculateSkrivelseDeadline throws on invalid input, consider validating here too (throw RangeError when either date is invalid) to avoid silently masking bad data.

Copilot uses AI. Check for mistakes.
Comment on lines +276 to +280
skrivelse_deadline: record.skrivelse_deadline,
days_overdue: daysOverdue(record.skrivelse_deadline, now),
risk_level: record.risk_level ?? 'MEDIUM',
riksdag_url: record.riksdag_url,
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectOverdueAlerts defaults missing risk_level to MEDIUM, while filterByMinRiskLevel treats missing risk_level as LOW. Having two different defaults makes downstream logic harder to reason about (e.g., alerts vs. filtering). Consider using a single shared default (and ideally a constant) for missing risk.

Copilot uses AI. Check for mistakes.
Comment thread scripts/rir-followups-client.ts Outdated
Comment on lines +524 to +525
* @param readFileFn - Injectable file reader (default: synchronous fs.readFileSync)
* @param filePath - Absolute or relative path to the JSON file
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc for loadRirDataset lists parameters in the opposite order of the actual function signature (it documents readFileFn before filePath). This can mislead callers and generated docs; please align the @param order/names with the signature.

Suggested change
* @param readFileFn - Injectable file reader (default: synchronous fs.readFileSync)
* @param filePath - Absolute or relative path to the JSON file
* @param filePath - Absolute or relative path to the JSON file
* @param readFileFn - Injectable file reader (default: synchronous fs.readFileSync)

Copilot uses AI. Check for mistakes.
@pethers
Copy link
Copy Markdown
Member

pethers commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

…ived status, daysOverdue validation, shared default risk, JSDoc and --date-arg fixes

Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/4d01721c-5d6a-483c-880e-e27413d49302

Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot apply changes based on the comments in this thread

Addressed in ab9fc42. All 7 round-2 review items: --date missing-value now exits 2 with clear error; fetchRiksdagSkrivelser paginates via p=N until exhausted (50-page safety cap); matchSkrivelse compares beteckning to response_skrivelse_id after normalising "Skr." prefix + whitespace; matched-skrivelse persisted status now goes through deriveResponseStatus (so PARTIAL with open_recommendations > 0 stays PARTIAL); daysOverdue throws RangeError on invalid input + 2 new negative tests; introduced DEFAULT_RISK_LEVEL = 'MEDIUM' constant shared by detectOverdueAlerts and filterByMinRiskLevel; loadRirDataset JSDoc @param order now matches signature. 68 RiR tests pass, tsc and eslint clean.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lighthouse Performance Audit

Category Score Status
Performance 85/100 🟡
Accessibility 95/100 🟢
Best Practices 90/100 🟢
SEO 95/100 🟢

📥 Download full Lighthouse report

Budget Compliance: Performance budgets enforced via budget.json

@pethers pethers merged commit ecfe0aa into main Apr 27, 2026
17 checks passed
@pethers pethers deleted the copilot/add-riksrevisionen-tracker branch April 27, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation updates javascript JavaScript code changes refactor Code refactoring schema Data schema changes size-xl Extra large change (> 1000 lines) size-xs Extra small change (< 10 lines) skill Skill configuration testing Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Methodology] Riksrevisionen follow-up tracker with skrivelse deadlines & accountability monitoring

3 participants